Cómo crear un sencillo juego con Javascript y el elemento canvas de HTML5


Matt Hackett publicó Cómo hacer un sencillo juego en HTML5 Canvas en agosto del 2011 El tutorial explica cómo construir un juego muy básico usando Javascript y el elemento canvas. El juego consiste en un guerrero que mata indefensos monstruos sin piedad y tiene el siguiente aspecto:



A continuación traduzco libremente el artículo al castellano, respetando al máximo el estilo desenfadado original del autor.

Tras el artículo original, incluyo algunas modificaciones sugeridas en los comentarios del artículo original y mis propias modificaciones al juego.

El código original del juego creado por Matt, puede descargarse desde GitHub.

Puedes jugar a mi versión del juego o echarle un vistazo al código Javascript en Snipt.net.
ACTUALIZADO: [29/01/2013] He subido el código con todas las modificaciones a mi cuenta de GitHub.


Crea el canvas

// Create the canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 480;
document.body.appendChild(canvas);
Lo primero que tenemos que hacer es crear el elemento canvas. En este caso, lo he hecho en Javascript en vez de HTML para demostrar lo fácil que es conseguirlo. Cuando tenemos el elemento, obtenemos una referencia a su contexto, establecemos sus dimensiones y lo añadimos al body del documento.

Incluye imágenes

// Background image
var bgReady = false;
var bgImage = new Image();
bgImage.onload = function () {
    bgReady = true;
};
bgImage.src = "images/background.png";
¡Un juego necesita gráficos! Así que vamos a cargar algunas imágenes. Quería algo simple, de manera que utilizo una simple image en vez de crear una class o algo mas complicado. bgReady se usa para hacer saber al canvas cúando es seguro dibujar sobre él, ya que si intentamos dibujar antes de que sea cargado obtendremos un error de DOM.

Repetimos el proceso para las tres gráficos que necesitamos: fondohéroe y monstruo.

Objetos del juego

// Game objects
var hero = {
    speed: 256, // movement in pixels per second
    x: 0,
    y: 0
};
var monster = {
    x: 0,
    y: 0
};
var monstersCaught = 0;
A continuación definimos algunas variables que usaremos más tarde. Configuramos al héroe (hero) especificando su velocidad (speed), que indica lo rápido que se moverá en píxeles por segundo. El monstruo (monster) no se moverá, por lo que sólo especificamos las coordenadas de su posición.

Finalmente, la variable monstersCaught (monstruos atrapados) almacena el número de monstruos que ha atrapado el héroe.

Entradas del jugador

// Handle keyboard controls
var keysDown = {};

addEventListener("keydown", function (e) {
    keysDown[e.keyCode] = true;
}, false);

addEventListener("keyup", function (e) {
    delete keysDown[e.keyCode];
}, false);
Ahora vamos con la gestión de las entradas del jugador. Esta es la parte que más sorprende a los desarrolladores que vienen del desarrollo web. Lo importante a recordar aquí es que no es necesario reaccionar a un evento de entrada por parte del usuario justo cuando ocurre. En un entorno web puede ser apropiado empezar una animación o solicitar datos en cuanto el usuario inicia la entrada de datos. Pero en el desarrollo de juegos queremos que la lógica del juego se encuentre en un solo lugar para controlar con precisión cuándo pasa algo, si pasa. Por este motivo almacenamos la entrada del usuario para más tarde.
Para ello utilizamos la variable keysDown que almacena el evento keyCode. Si el keyCode está almacenado en el objeto keysDown significa que el usuario está pulsando esa tecla. ¡Así de simple!

Nuevo juego

// Reset the game when the player catches a monster
var reset = function () {
    hero.x = canvas.width / 2;
    hero.y = canvas.height / 2;

    // Throw the monster somewhere on the screen randomly
    monster.x = 32 + (Math.random() * (canvas.width - 64));
    monster.y = 32 + (Math.random() * (canvas.height - 64));
};
La función reset se llama siempre que se empieza una nueva partida, o nivel, o como quieras llamarlo. Coloca el héroe (el jugador) en el centro de la pantalla y el monstruo en algún lugar al azar.

Actualiza los objetos

// Update game objects
var update = function (modifier) {
    if (38 in keysDown) { // Player holding up
        hero.y -= hero.speed * modifier;
    }
    if (40 in keysDown) { // Player holding down
        hero.y += hero.speed * modifier;
    }
    if (37 in keysDown) { // Player holding left
        hero.x -= hero.speed * modifier;
    }
    if (39 in keysDown) { // Player holding right
        hero.x += hero.speed * modifier;
    }

    // Are they touching?
    if (
        hero.x <= (monster.x + 32)
        && monster.x <= (hero.x + 32)
        && hero.y <= (monster.y + 32)
        && monster.y <= (hero.y + 32)
    ) {
        ++monstersCaught;
        reset();
    }
};
Esta es la función update (actualiza), llamada en cada intervalo de la ejecución del juego. Lo primero que hace es comprobar las teclas ARRIBA, ABAJO, IZQUIERDA, DERECHA para ver si el usuario las ha pulsado. Si es así, el héroe se mueve en la dirección adecuada.

Lo que puede parecer extraño es el parámetro modifier que se pasa a la función update. Puedes observar que se hace referencia a modifier en la función principal main, pero déjame explicar qué hace ese modifier aquí. modifier es un número relacionado con el tiempo. Si ha pasado exactamente un segundo, el valor de modifier será 1 y la velocidad del héroe se multiplicará por 1, es decir, que se habrá movido 256 píxeles ese segundo. Si sólo ha pasado medio segundo, el valor será 0.5, por lo que el héroe se habrá movido a la mitad de su velocidad en el intervalo de tiempo. Esta función se llama tan rápidamente que el valor de modifier será normalmente muy bajo, pero es una buena práctica que asegura que el héroe se moverá siempre a la misma velocidad, independientemente de lo rápido (o lento) que se ejecute el script del juego.

Ahora que hemos movido el héroe de acuerdo con la entrada del jugador, tenemos que comprobar qué ha pasado con ese movimiento. Si ha habido una colisión entre el héroe y el monstruo, ¡éso es todo! De eso va el juego, básicamente. Incrementamos la puntuación (+1 a monstersCaught) y reiniciamos el juego.

Dibuja los objetos

// Draw everything
var render = function () {
    if (bgReady) {
        ctx.drawImage(bgImage, 0, 0);
    }

    if (heroReady) {
        ctx.drawImage(heroImage, hero.x, hero.y);
    }

    if (monsterReady) {
        ctx.drawImage(monsterImage, monster.x, monster.y);
    }

    // Score
    ctx.fillStyle = "rgb(250, 250, 250)";
    ctx.font = "24px Helvetica";
    ctx.textAlign = "left";
    ctx.textBaseline = "top";
    ctx.fillText("Goblins cazados: " + monstersCaught, 32, 32);
};
Los juegos son más divertidos cuando ves qué está pasando, así que vamos a dibujarlo todo sobre la pantalla. En primer lugar, tomamos la imagen de fondo y la dibujamos sobre el canvas. Repetimos para dibujar el héroe y el monstruo. Nota: Recuerda que el orden es importante, ya que cualquier imagen dibujada en el canvas sobreescribe los píxeles que hay debajo.

A continuación cambiamos algunas propiedades del contexto relacionadas, especificamos detalles sobre la fuente y hacemos una llamada a fillText para mostrar la puntuación del jugador. Como no tenemos movimientos o animaciones complicadas, eso es todo lo que tenemos que dibujar.

El bucle principal del juego

// The main game loop
var main = function () {
    var now = Date.now();
    var delta = now - then;

    update(delta / 1000);
    render();

    then = now;
};
El bucle principal controla el flujo del juego. Primero obtenemos el valor actual del reloj de manera que podamos calcular cúantos milisegundos han pasado desde el último intervalos en la variable delta. Obtenemos el valor del parámetro modifier que pasamos a la función update dividido por 1000 (el número de milisegundos que tiene un segundo). Después, llamamos a render y guardamos de nuevo la hora.

Échale un vistazo a Onslaught! Arena Case Study para más detalles sobre bucles en juegos.

¡Arranca el juego!

// Let's play this game!
reset();
var then = Date.now();
setInterval(main, 1); // Execute as fast as possible
¡Ya casi estamos, éste es el último trozo de código! Primero llamamos a la función reset para empezar una nueva partida/nivel. (Recuerda que ésto centra al héroe y coloca al monstruo en un lugar al azar para que el jugador lo encuentre). Después, obtenemos la hora (la guardamos en la variable then) y empezamos el intervalo.

¡Felicidades! Ahora ya comprendes (¡eso espero!) los fundamentos básicos del desarrollo de juegos utilizando el elemento canvas y Javascript. ¡Pruébalo por tí mismo! Juega al juego o forkea el código en GitHub y empieza a trastear.

¿Tienes comentarios?

¿Qué te ha parecido el tutorial? ¿Te ha servido de ayuda? ¿Demasiado lento, demasiado rápido, demasiado técnico, no lo suficientemente técnico? ¡Por favor, házselo saber a su autor para que pueda mejorar en su próximo tutorial! Puedes seguirlo en Twitter para saber cúando estará disponible su próximo tutorial.

Modificaciones en los comentarios del artículo original

En los comentarios al tutorial original hay algunas modificaciones que pueden ser interesantes.

//this variable is true when the game starts, false from that point on
var start = true;
// Reset the game when the player catches a monster
var reset = function () {
if (start){
    hero.x = canvas.width / 2;
    hero.y = canvas.height / 2;
    start = false;
}
// Throw the monster somewhere on the screen randomly
monster.x = 32 + (Math.random() * (canvas.width - 64));
monster.y = 32 + (Math.random() * (canvas.height - 64));
};
Drew Long sugiere una modificación de la función reset para evitar que el héroe vuelva al centro de la pantalla después de cazar al goblin.

La idea es utilizar la variable start para controlar cuando es el primera vez que se ejecuta el juego y así centrar el héroe. Después de la primera vez, no se resetea la posición del héroe.

if (38 in keysDown) { // Player holding up
    hero.y = (hero.y > 0) ? (hero.y - hero.speed * modifier) : canvas.height - 32;
}

if (40 in keysDown) { // Player holding down
    hero.y = (hero.y + hero.speed * modifier) % canvas.height;
}

if (37 in keysDown) { // Player holding left
    hero.x = (hero.x > 0) ? (hero.x - hero.speed * modifier) : canvas.width - 32;
}

if (39 in keysDown) { // Player holding right
    hero.x = (hero.x + hero.speed * modifier) % canvas.width;
}
Esta modificación de Ryan Kane hace que el héroe aparezca por el lado opuesto por el que ha salido del canvas. Cuando el héroe se mueve, se comprueba si la posición es superior a los límites del canvas. Si es así, se le hace aparecer en la lado opuesto. Si no, se mueve de manera normal.

Finalmente Pooya ha modificado el juego original haciendo que el goblin se mueva en vertical, apareciendo de nuevo por la parte superior cuando sale del canvas por la parte inferior.

Mis propias modificaciones

He colgado mi versión del juego en mi cuenta en GitHub.

Además de incluir las modificaciones anteriores, quería que el mostruo tuviera un movimiento más natural que el definido por Pooya. Mi idea es hacer que el monstruo "huya" del héroe. 

if (TECLA_ARRIBA in keysDown) { // Player holding up
        hero.y = (hero.y > 0) ? (hero.y - hero.speed * modifier) : canvas.height - 32;
        monster.y = ( monster.y > 0 ) ? ( monster.y - monster.speed * modifier ) : canvas.height - 32;
    }

Como ves, simplemente he duplicado el código que hace que se mueva el héroe. Aunque sólo muestro la línea introducida en la función update para la línea en la que se comprueba la pulsación de la tecla arriba, también debe introducirse para la comprobación del resto de teclas de dirección.

En la definición del monstruo, he definido una velocidad para el monstruo, de forma análoga a como se define para el héroe:

var monster = {

    speed : 5  // movement in pixels per second

};

Aumentando la dificultad

En todos los juegos la dificultad aumenta a medida que pasa el tiempo. En este sencillo juego, una manera de  incrementar la dificultad es haciendo que el monstruo huya cada vez más rápidamente.

monster.speed = (monster.speed > 100 ) ? ( monster.speed) : (monster.speed + monstersCaught);

En este caso, si la velocidad del monstruo es superior a 100, la velocidad del monstruo ya no aumenta más. En caso contrario, aumentamos la velocidad sumando el número de monstruo que lleva cazados el héroe. De esta forma cada vez resulta un poquito más dificil pillar al monstruo.

Si no ponemos un máximo a la velocidad que puede alcanzar el monstruo, llegaría un momento en el que el monstruo se movería más rápido que el héroe y ¡no podría atraparlo nunca!

Añadiendo tumbas

Cada vez que el héroe caza al monstruo, aparece un nuevo monstruo en otro lugar al azar del canvas. Sin embargo, quería introducir algún elemento que indicara la progresión del juego (además de la mayor velocidad del monstruo al huir).

La solución ha sido introducir una lápida en el lugar donde se ha cazado al monstruo.


He definido una variable llamada monsterGraveyad (cementerio de monstruos) donde guardo la posición donde el héroe caza al monstruo. Así, cuando se detecta una colisión entre el héroe y el monstruo, aumentamos la puntuación y guardo la posición del encuentro mediante:

monsterGraveyard.push({"x": monster.x, "y": monster.y });

En la función render(), después de dibujar el fondo, recorro todos los elementos de monsterGraveyard y coloco una lápida en cada uno de los lugares donde murió un monstruo.

if (deadMonsterReady) {
    for (deadMonster in monsterGraveyard) {
        ctx.drawImage( deadMonsterImage,
                       monsterGraveyard[deadMonster].x,
                       monsterGraveyard[deadMonster].y)
    }
}
He dibujado la imagen de la lápida y la he guardado en el fichero images/deadmonster.png. Toda la información relativa a esta imagen está almacenada en la variable deadMonsterImage (el código es el mismo que para el resto de imágenes del juego).

Conclusiones

Gracias a la simplicidad del juego propuesto por Matt, es muy fácil comprender cómo utilizar el elemento canvas para diseñar un juego que se ejecute en el navegador. 

Con muy poco esfuerzo adicional, podemos añadir nuevas funcionalidades, como que el monstruo huya del héroe o introducir nuevos elementos en el juego (en mi caso, las lápidas).

A partir de aquí, el límite lo pone tu imaginación (y tus ganas de aprender Javascript). Puedes incluir más monstruos, puntuación variable en función del tiempo que el héroe tarda en dar caza al monstruo, armas a larga distancia o cualquier otra cosa.

¿Te animas?

Comentarios

Anónimo ha dicho que…
Aqui explican de manera sencilla como hacer un juego http://youtu.be/DYC9FXKUkQc